<?php
namespace libs\qoy\DB;
use PDO;
use libs\qoy\Exception\ExceptionInternalServerError;
use libs\qoy\DI;

require_once API_ROOT . '/libs/NotORM' . DIRECTORY_SEPARATOR . 'NotORM.php';


/**
 * DBNotORM 分布式的DB存储
 *
 * 基于NotORM的数据库操作，支持分布式
 * 
 * - 可定义每个表的存储路由和规则，匹配顺序：
 *   自定义区间匹配 -> 自定义缺省匹配 -> 默认区间匹配 -> 默认缺省匹配
 * - 底层依赖NotORM实现数据库的操作
 * @param array $configs 数据库配置 
 * @param array $configs["servers"] 可用的DB服务器集群
 * @param array $configs["servers"]["*"] 可用的DB服务器集群对象配置
 * @param array $configs["servers"]["*"]["host"] 数据库域名
 * @param array $configs["servers"]["*"]["name"] 数据库名字
 * @param array $configs["servers"]["*"]["user"] 数据库用户名
 * @param array $configs["servers"]["*"]["password"] 数据库密码
 * @param array $configs["servers"]["*"]["port"] 数据库端口
 * @param array $configs["servers"]["*"]["charset"] 数据库字符集
 * @param array $configs["tables"] 表路由
 * @param array $configs["tables"]['*'] 表路由项
 * @param array $configs["tables"]["*"]["prefix"] 表前缀
 * @param array $configs["tables"]["*"]["key"] 区间key
 * @param array $configs["tables"]["*"]["map"] 表路由map
 * @param array $configs["tables"]["*"]["map"]["*"] 区间
 * @param array $configs["tables"]["*"]["map"]["*"]["start"] 区间
 * @param array $configs["tables"]["*"]["map"]["*"]["end"] 区间
 * @param array $configs["tables"]["*"]["map"]["*"]["db"] 区间
 * @param boolean $configs['debug'] 是否开启调试模式
 */
abstract class DBNotORM {

    
   
    
    
    /**
     * @var NotORM $_notorms NotORM的实例池
     */
    protected $_notorms = array();

    /**
     * @var PDO $_pdos PDO连接池，统一管理，避免重复连接
     */
    protected $_pdos = array();

    /**
     * @var array $_configs 数据库配置 
     */
    protected $_configs = array();
    /**
     * 
     * @var array 
     */
    protected static $tableKeys = array();

    /**
     * @var boolean 是否开启调试模式，调试模式下会输出全部执行的SQL语句和对应消耗的时间
     */
    protected $debug = FALSE;

    /**
     * @var boolean 是否保持原来数据库结果集中以主键为KEY的返回方式（默认不使用）
     */
    protected $isKeepPrimaryKeyIndex = FALSE;

    /**
     * @param array $configs 数据库配置 
     * @param array $configs["servers"] 可用的DB服务器集群
     * @param array $configs["servers"]["*"] 可用的DB服务器集群对象配置
     * @param array $configs["servers"]["*"]["host"] 数据库域名
     * @param array $configs["servers"]["*"]["name"] 数据库名字
     * @param array $configs["servers"]["*"]["user"] 数据库用户名
     * @param array $configs["servers"]["*"]["password"] 数据库密码
     * @param array $configs["servers"]["*"]["port"] 数据库端口
     * @param array $configs["servers"]["*"]["charset"] 数据库字符集
     * @param array $configs["tables"] 表路由
     * @param array $configs["tables"]['*'] 表路由项
     * @param array $configs["tables"]["*"]["prefix"] 表前缀
     * @param array $configs["tables"]["*"]["key"] 区间key
     * @param array $configs["tables"]["*"]["map"] 表路由map
     * @param array $configs["tables"]["*"]["map"]["*"] 区间
     * @param array $configs["tables"]["*"]["map"]["*"]["start"] 区间
     * @param array $configs["tables"]["*"]["map"]["*"]["end"] 区间
     * @param array $configs["tables"]["*"]["map"]["*"]["db"] 区间
     * @param \NotORM_Cache $configs['cache'] 缓存
     * @param boolean $configs['debug'] 是否开启调试模式
     */
    public function __construct($configs=[]) {
        $this->_configs = $configs;
        if(empty($this->_configs)){
           $this->_configs=DI::instance()->config->get("dbs");
        }
        
        $this->debug = isset($this->_configs['debug']) ? $this->_configs['debug'] : FALSE;
        
    }
    /**
     * 
     * @param string $name 表名
     * @return NotORM_Result NotORM表对象
     */
    public function __get(string $name) {
        $notormKey = $this->createNotormKey($name);

        if (!isset($this->_notorms[$notormKey])) {
            list($tableName, $suffix) = $this->parseName($name);
            $router = $this->getDBRouter($tableName, $suffix);
               
            $structure = new \NotORM_Structure_Convention(
                    $router['key'], '%s_id', '%s', $router['prefix']);
            $this->_notorms[$notormKey] = new \NotORM($router['pdo'], $structure);

            $this->_notorms[$notormKey]->debug = $this->debug;
            $this->_notorms[$notormKey]->isKeepPrimaryKeyIndex = $this->isKeepPrimaryKeyIndex;

            if ($router['isNoSuffix']) {
                $name = $tableName;
            }
        }

        return $this->_notorms[$notormKey]->$name;
    }

    public function __set($name, $value) {
        foreach ($this->_notorms as $key => $notorm) {
            $notorm->$name = $value;
        }
    }

    protected function createNotormKey($tableName) {
        return '__' . $tableName . '__';
    }

    /**
     * 解析分布式表名
     * 表名  + ['_' + 数字后缀]，如：user_0, user_1, ... user_100
     * @param string $name
     */
    protected function parseName($name) {
        $tableName = $name;
        $suffix = NULL;

        $pos = strrpos($name, '_');
        if ($pos !== FALSE) {
            $tableId = substr($name, $pos + 1);
            if (is_numeric($tableId)) {
                $tableName = substr($name, 0, $pos);
                $suffix = intval($tableId);
            }
        }

        return array($tableName, $suffix);
    }

    /**
     * 获取分布式数据库路由
     * @param string $tableName 数据库表名
     * @param string $suffix 分布式下的表后缀
     * @return array 数据库配置
     * @throws ExceptionInternalServerError
     */
    protected function getDBRouter($tableName, $suffix) {
        $rs = array('prefix' => '', 'key' => '', 'pdo' => NULL, 'isNoSuffix' => FALSE);

        $defaultMap = !empty($this->_configs['tables']['__default__']) ? $this->_configs['tables']['__default__'] : array();
        $tableMap = !empty($this->_configs['tables'][$tableName]) ? $this->_configs['tables'][$tableName] : $defaultMap;

        if (empty($tableMap)) {
            throw new ExceptionInternalServerError(ExceptionInternalServerError::NO_TABLE_MAP_CONFIG, array('tableName' => $tableName));
        }

        $dbKey = NULL;
        $dbDefaultKey = NULL;
        if (!isset($tableMap['map'])) {
            $tableMap['map'] = array();
        }
        foreach ($tableMap['map'] as $map) {
            $isMatch = FALSE;

            if ((isset($map['start']) && isset($map['end']))) {
                if ($suffix !== NULL && $suffix >= $map['start'] && $suffix <= $map['end']) {
                    $isMatch = TRUE;
                }
            } else {
                $dbDefaultKey = $map['db'];
                if ($suffix === NULL) {
                    $isMatch = TRUE;
                }
            }

            if ($isMatch) {
                $dbKey = isset($map['db']) ? trim($map['db']) : NULL;
                break;
            }
        }
        //try to use default map if no perfect match
        if ($dbKey === NULL) {
            $dbKey = $dbDefaultKey;
            $rs['isNoSuffix'] = TRUE;
        }

        if ($dbKey === NULL) {
            throw new ExceptionInternalServerError(ExceptionInternalServerError::NO_DB_ROUTER_MATCH_FOR_TABLE, array('db' => $dbKey));
            
// //           throw new PhalApi_Exception_InternalServerError(
////            T('No db router match for {tableName}', array('tableName' => $tableName))
////            );
        }

        $rs['pdo'] = $this->getPdo($dbKey);
        $rs['prefix'] = isset($tableMap['prefix']) ? trim($tableMap['prefix']) : '';
        $rs['key'] = isset($tableMap['key']) ? trim($tableMap['key']) : 'id';

        return $rs;
    }

    /**
     * 获取 PDO连接
     * @param string $dbKey 数据库表名唯一KEY
     * @return PDO
     */
    protected function getPdo($dbKey) {
        if (!isset($this->_pdos[$dbKey])) {
            $dbCfg = isset($this->_configs['servers'][$dbKey]) ? $this->_configs['servers'][$dbKey] : array();

            if (empty($dbCfg)) {
                throw new ExceptionInternalServerError(ExceptionInternalServerError::NO_SUCH_DB_IN_SERVERS, array('db' => $dbKey));
//                throw new PhalApi_Exception_InternalServerError(
//                T('no such db:{db} in servers', array('db' => $dbKey)));
            }

            try {
                $this->_pdos[$dbKey] = $this->createPDOBy($dbCfg);
            } catch (PDOException $ex) {
                //异常时，接口异常返回，并隐藏数据库帐号信息
                throw new ExceptionInternalServerError(ExceptionInternalServerError::CAN_NOT_CONNECT_TO_DATABASE, array('db' => $dbKey));
//                throw new PhalApi_Exception_InternalServerError(
//                T('can not connect to database:{db}', array('db' => $dbKey)));
            }
        }

        return $this->_pdos[$dbKey];
    }

    /**
     * 针对MySQL的PDO链接，如果需要采用其他数据库，可重载此函数
     * @param array $dbCfg 数据库配置
     * @return PDO
     */
    abstract protected function createPDOBy($dbCfg) ;

    /**
     * 断开数据库链接
     */
    public function disconnect() {
        foreach ($this->_pdos as $dbKey => $pdo) {
            $this->_pdos[$dbKey] = NULL;
            unset($this->_pdos[$dbKey]);
        }

        foreach ($this->_notorms as $notormKey => $notorm) {
            $this->_notorms[$notormKey] = NULL;
            unset($this->_notorms[$notormKey]);
        }
    }

    /**
     * 为历史修改埋单：保持原来数据库结果集中以主键为KEY的返回方式
     *
     * - PhalSpi 1.3.1 及以下版本才需要用到此切换动作
     * - 涉及影响的数据库操作有：fetchAll()/fetchRows()等
     *
     * ＠return PhalApi_DB_NotORM
     */
    public function keepPrimaryKeyIndex() {
        $this->isKeepPrimaryKeyIndex = TRUE;
        return $this;
    }

    /** ------------------ 事务操作 ------------------ * */

    /**
     * 开启数据库事务
     * @param string $whichDB 指定数据库标识
     * @return NULL
     */
    public function beginTransaction($whichDB) {
        $this->getPdo($whichDB)->beginTransaction();
    }

    /**
     * 提交数据库事务
     * @param string $whichDB 指定数据库标识
     * @return NULL
     */
    public function commit($whichDB) {
        $this->getPdo($whichDB)->commit();
    }

    /**
     * 回滚数据库事务
     * @param string $whichDB 指定数据库标识
     * @return NULL
     */
    public function rollback($whichDB) {
        $this->getPdo($whichDB)->rollback();
    }
    
    
    protected function loadTableKeys() {
        $tables = $this->_configs["tables"];
        if (empty($tables)) {
            throw new ExceptionInternalServerError(ExceptionInternalServerError::TABLES_SHOULD_NOT_BE_EMPTY);//dbs.tables should not be empty
        }

        foreach ($tables as $tableName => $tableConfig) {
            if (isset($tableConfig['start']) && isset($tableConfig['end'])) {
                for ($i = $tableConfig['start']; $i <= $tableConfig['end']; $i ++) {
                    self::$tableKeys[$tableName . '_' . $i] = $tableConfig['key'];
                }
            } else {
                self::$tableKeys[$tableName] = $tableConfig['key'];
            }
        }
    }
    
    
      /**
     * 根据表名获取主键名
     *
     * - 考虑到配置中的表主键不一定是id，所以这里将默认自动装配数据库配置并匹配对应的主键名
     * - 如果不希望因自动匹配所带来的性能问题，可以在每个实现子类手工返回对应的主键名
     * - 注意分表的情况
     * 
     * @param string $table 表名/分表名
     * @return string 主键名
     */
      public function getTableKey($table) {
        if (empty(self::$tableKeys)) {
            $this->loadTableKeys();
        }
        return isset(self::$tableKeys[$table]) ? self::$tableKeys[$table] : self::$tableKeys['__default__'];
    }


}
